require "erb"
require "test/unit/failure"
require "test/unit/error"
require "test/unit/testresult"
require "test/unit/testsuite"
require "test/unit/autorunner"

module Test
  module Unit
    module XMLReportable
      include ERB::Util

      def to_xml
        <<-XML.gsub(/\s*(\n  <\/test>|\n<\/result>)/m, "\\1")
<result>
  <test_case>
    <name>#{h(@test.class.name)}</name>
    <description/>
  </test_case>
  <test>
    <name>#{h(@test.method_name)}</name>
    <description/>
#{options_xml}
  </test>
  <status>#{h(status_name)}</status>
  <detail>#{h(message)}</detail>
  <elapsed>#{h(elapsed_time)}</elapsed>
#{backtrace_xml}
</result>
XML
      end

      private
      def options_xml
        @test.attributes.collect do |key, value|
          <<-XML
    <option>
      <name>#{h(key)}</name>
      <value>#{h(value)}</value>
    </option>
XML
        end.join
      end

      def backtrace_xml
        entries = backtrace_entries_xml
        if entries.empty?
          ""
        else
          <<-XML
  <backtrace>
#{entries.join.rstrip}
  </backtrace>
XML
        end
      end

      def backtrace_entries_xml
        location.collect do |location|
          file, line, info = location.split(/:(\d+):/)
          <<-XML
    <entry>
      <file>#{h(file)}</file>
      <line>#{h(line)}</line>
      <info>#{h(info.to_s.strip)}</info>
    </entry>
XML
        end
      end
    end

    class Success
      include XMLReportable

      attr_reader :test, :elapsed_time
      def initialize(test, elapsed_time)
        @test = test
        @elapsed_time = elapsed_time
      end

      def message
        nil
      end

      def location
        []
      end

      def test_name
        @test.name
      end

      def test_case_name
        /\((.*)\)\z/ =~ test_name
        $1
      end

      def status_name
        "success"
      end
    end

    class Failure
      include XMLReportable

      attr_accessor :test, :elapsed_time

      def status_name
        "failure"
      end
    end

    class Error
      include XMLReportable

      attr_accessor :test, :elapsed_time

      def status_name
        "error"
      end

      def location
        filter_backtrace(@exception.backtrace)
      end
    end

    class TestCase
      alias_method(:run_without_success_notify, :run)
      def run(result, &block)
        @_start_time = Time.now
        run_result = run_without_success_notify(result, &block)
        result.add_success(Success.new(self, Time.now - @_start_time)) if passed?
        run_result
      end

      alias_method(:add_failure_without_test_case_set, :add_failure)
      def add_failure(*args)
        add_failure_without_test_case_set(*args)
        failure = @_result.failures.last
        failure.test = self
        failure.elapsed_time = Time.now - @_start_time
      end

      alias_method(:add_error_without_test_case_set, :add_error)
      def add_error(*args)
        add_error_without_test_case_set(*args)
        error = @_result.errors.last
        error.test = self
        error.elapsed_time = Time.now - @_start_time
      end
    end

    class TestResult
      attr_reader :failures, :errors

      alias_method(:initialize_without_successes, :initialize)
      def initialize
        initialize_without_successes
        @successes = []
        @logs = []
      end

      def add_success(success)
        @logs << success
        @successes << success
      end

      alias_method(:add_failure_without_logs_store, :add_failure)
      def add_failure(failure)
        @logs << failure
        add_failure_without_logs_store(failure)
      end

      alias_method(:add_error_without_logs_store, :add_error)
      def add_error(error)
        @logs << error
        add_error_without_logs_store(error)
      end

      def to_xml
        return "<report/>" if @logs.empty?
        xml = @logs.collect {|log| log.to_xml.gsub(/^/, "  ")}.join
        "<report>\n#{xml}</report>\n"
      end
    end

    class TestSuite
      attr_reader :result

      alias_method(:run_without_keep_result, :run)
      def run(result, &block)
        @result = result
        run_without_keep_result(result, &block)
      end
    end

    class AutoRunner
      alias_method(:options_without_xml_report_support, :options)
      def options
        opt = options_without_xml_report_support
        @xml_report_support_option_added ||= false
        unless @xml_report_support_option_added
          @xml_report_file = nil
          opt.on('--xml-report=FILE',
                 "Output test report in XML to FILE.") do |file|
            @xml_report_file = file
          end
        end
        opt
      end

      alias_method(:run_without_xml_report_support, :run)
      def run
        passed = run_without_xml_report_support
        if @xml_report_file
          File.open(@xml_report_file, "w") do |f|
            f.print(@suite.result.to_xml)
          end
        end
        passed
      end
    end
  end
end
